SQLiteは軽量で高速ですが、マルチスレッド環境ではロックが発生しやすいという特徴があります。 特に C#(Microsoft.Data.Sqlite)で業務アプリを作る場合、 database is locked を防ぐ設計が必須です。
この記事でわかること
・SQLiteがマルチスレッドでロックしやすい理由
・スレッド安全な書き込み設計(直列化)
・WALモードの有効化
・書き込みキュー(Producer/Consumer)
・非同期処理との組み合わせ
・業務アプリ向けベストプラクティス
・SQLiteがマルチスレッドでロックしやすい理由
・スレッド安全な書き込み設計(直列化)
・WALモードの有効化
・書き込みキュー(Producer/Consumer)
・非同期処理との組み合わせ
・業務アプリ向けベストプラクティス
1. SQLiteは「同時書き込みが1つだけ」
SQLiteは単一ファイルDBのため、 同時に複数の書き込みを処理できません。
- 読み込み(SELECT)は複数同時OK
- 書き込み(INSERT/UPDATE/DELETE)は同時1つだけ
database is locked の9割は「同時書き込み」が原因。
2. マルチスレッドで安全にする基本戦略
SQLiteを安全に使うには、次の3つが必須です。
- 書き込みを直列化する(排他制御)
- WALモードを有効化する
- トランザクションを短く保つ
3. 書き込みは lock で直列化する(最重要)
複数スレッドから書き込みが来る場合、 必ず1つずつ順番に処理する必要があります。
■ C#での排他制御
private static readonly object _writeLock = new();
public void UpdateUser(User user)
{
lock (_writeLock)
{
using var con = new SqliteConnection(_cs);
con.Open();
var sql = "UPDATE Users SET Name=@name WHERE Id=@id";
using var cmd = new SqliteCommand(sql, con);
cmd.Parameters.AddWithValue("@name", user.Name);
cmd.Parameters.AddWithValue("@id", user.Id);
cmd.ExecuteNonQuery();
}
}
ポイント
- 書き込みは lock で1つずつ
- 読み込みは lock 不要(基本的に)
4. 書き込みキュー(Producer/Consumer)方式
業務アプリでは、書き込みを専用スレッドに集約するのが最も安全です。 UIスレッドや複数スレッドからの書き込み要求はキューに積み、 バックグラウンドスレッドが順番に処理します。
■ 書き込みキューの例
private readonly BlockingCollection<Action> _writeQueue = new();
public void StartWriteWorker()
{
Task.Run(() =>
{
foreach (var action in _writeQueue.GetConsumingEnumerable())
{
action();
}
});
}
public void EnqueueWrite(Action action)
{
_writeQueue.Add(action);
}
■ 使い方
EnqueueWrite(() =>
{
using var con = new SqliteConnection(_cs);
con.Open();
var sql = "INSERT INTO Logs (Message) VALUES (@msg)";
using var cmd = new SqliteCommand(sql, con);
cmd.Parameters.AddWithValue("@msg", "ログ");
cmd.ExecuteNonQuery();
});
これで書き込みは常に1つずつになり、locked がほぼ消えます。
5. WALモードを有効化する(読み書き並列に強くなる)
SQLiteはデフォルトではロックしやすいですが、 WAL(Write-Ahead Logging)モードにすると改善します。
■ WALモードの設定
using var con = new SqliteConnection(_cs);
con.Open();
using var cmd = new SqliteCommand("PRAGMA journal_mode=WAL;", con);
cmd.ExecuteNonQuery();
WALのメリット
- 読み込みと書き込みが同時に可能
- ロックが大幅に減る
- 業務アプリではほぼ必須
6. トランザクションは短く・まとめて
トランザクション中は書き込みロックが保持されるため、 長時間のトランザクションは厳禁です。
■ 正しいパターン
using var tran = con.BeginTransaction();
try
{
// INSERT/UPDATE をまとめて実行
tran.Commit();
}
catch
{
tran.Rollback();
}
ポイント
- 大量INSERTは1トランザクションで一気に
- トランザクションは短く保つ
7. 非同期処理(async/await)との組み合わせ
UIスレッドでDBアクセスすると、 処理が詰まってロックが発生しやすくなります。
■ 非同期で書き込み
await cmd.ExecuteNonQueryAsync();
■ DataTable.Load は同期 → Task.Run で逃がす
await Task.Run(() => dt.Load(reader));
8. マルチスレッド安全設計の最終チェックリスト
- 書き込みは直列化(lock or 書き込みキュー)
- WALモードを有効化
- reader は必ず using で閉じる
- トランザクションは短く
- 大量INSERTは1トランザクションで高速化
- UIスレッドでDBアクセスしない(async/await)
- バックアップはオンラインバックアップAPI
まとめ:SQLiteは“設計次第で”マルチスレッドに強くなる
- SQLiteは同時書き込みに弱い → 設計で補う
- 書き込みキュー方式が最も安全で実務向け
- WALモードで読み書き並列に強くなる
- 排他制御+非同期+短いトランザクションが鍵
SQLiteは軽量で扱いやすい反面、 マルチスレッド設計を誤るとロック地獄になります。 この記事のパターンを採用すれば、 ロックしない・壊れない・高速なアプリを構築できます。